/*
 * Copyright 2009 Armando Blancas
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package taskgraph.tasks.text;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.EnumSet;
import java.util.Set;

import taskgraph.ports.CharInputPort;
import taskgraph.ports.CharOutputPort;
import taskgraph.tasks.PrimitiveTask;


/**
 * Counts the number of lines, words, and characters.
 * 
 * <p>In this task, a word is any grouping of consecutive letter, number, 
 * period or underscore, and anything else is a separator. This is the 
 * opposite approach than the one in the typical wc UNIX command, where a 
 * word is anything between lines, spaces and tabs. 
 * 
 * @author Armando Blancas
 * @see taskgraph.ports.CharInputPort
 * @see taskgraph.ports.CharOutputPort
 */
public class WordCount extends PrimitiveTask {
	
	public enum WC_F { LINES, WORDS, CHARACTERS };

	private static final Set<WC_F> DEFAULT_FLAGS = 
		EnumSet.of(WC_F.LINES, WC_F.WORDS, WC_F.CHARACTERS);  
	
	private CharInputPort input;
	private CharOutputPort output;
	private Set<WC_F> flags = DEFAULT_FLAGS;

	/**
	 * This constructor allows creating instances as beans.
	 */
    public WordCount() {
    	// do nothing
    }

	/**
     * Creates an instance for counting chars, words and lines.
     * 
     * @param input The char input port.
     * @param output The char output port.
     */
    public WordCount(final CharInputPort input, final CharOutputPort output) {
    	setInput(input);
    	setOutput(output);
    }

	/**
     * Creates a fully configured instance.
     * 
     * @param input The char input port.
     * @param output The char output port.
     * @param flags The set of choices.
     */
    public WordCount(final CharInputPort input, 
    		         final CharOutputPort output, 
    		         final Set<WC_F> flags) {
    	setInput(input);
    	setOutput(output);
    	setFlags(flags);
    }

    /**
	 * Counts the number of words in the passed string.
	 * Looks for what makes a word, anything else
	 * is a separator.
	 * 
	 * @param line The line of text as a {@code String}.
	 * @return The number of words in the string.
	 */
	private int countWords(String line) {
		int words = 0;
		boolean inWord = false;
		char[] chars = line.toCharArray();
		for (char c : chars) {			// Start or belong in a word:
			if ((c > 96 && c < 123) ||	// a-z
				(c > 64 && c < 91)  ||	// A-Z
				(c > 47 && c < 58)  ||	// 0-9
				(c == 46 || c == 95)) {	// . _
				inWord = true;
			}
			else {						// Anything else is a separator.
				if (inWord) {
					inWord = false;
					++words;
				}
			}
		}
		if (inWord) ++words;			// Word is a full line.
		return words;
	}

    /**
	 * Gets the set of command options.
	 * 
	 * @return The set of flags.
	 */
	public Set<WC_F> getFlags() {
		return flags;
	}
	
    /**
     * Gets the input port.
     * 
	 * @return The char input port.
	 */
	public CharInputPort getInput() {
		return input;
	}
	
	/**
     * Gets the output port.
     * 
	 * @return The char output port.
	 */
	public CharOutputPort getOutput() {
		return output;
	}
	
	/**
	 * Prints the results to the output port.
	 * 
	 * @param lines The line count.
	 * @param words The word count.
	 * @param chars The char count.
	 * @throws IOException If there's an error in the port.
	 */
	private void printResults(int lines, int words, int chars) 
	throws IOException {
		String space = "";
		
		if (flags.contains(WC_F.LINES)) {
			output.printf("%7d", lines);
			space = " ";
		}
		if (flags.contains(WC_F.WORDS)) {
			output.printf("%s%7d", space, words);
			space = " ";
		}
		if (flags.contains(WC_F.CHARACTERS)) {
			output.printf("%s%7d", space, chars);
		}
		output.println();
	}

	/**
     * Reads lines from the input port counting lines, wornds, and characters.
	 *<pre>
	 *while not EOF
	 *    read a line from input port
	 *    increase line count
	 *    count words and add them to word count
	 *    count chars and add them to char count
	 *close input port
	 *close output port
	 *</pre>
	 */
	@Override
    public void run() {
    	try {
    		int lines = 0;
    		int words = 0;
    		int chars = 0;
    		while (!Thread.currentThread().isInterrupted()) {
    			String line = input.readLine();
    			if (line == null) {
    				break;
    			}
    			++lines;    			
    			chars += line.length();
    			
    			if (flags.contains(WC_F.WORDS)) {
    				words += countWords(line);
    			}
            }
    		
    		printResults(lines, words, chars);

        } catch (InterruptedIOException ie) {
        	// interruption request
        } catch (IOException ioe) {
        	// pipe synch error
            log.error("WordCount#run()", ioe);
        } finally {
			input.close();
			output.close();
        }
    }
	
	/**
	 * Sets the commands options for counting.
	 * 
	 * @param flags The set of flags.
	 */
	public void setFlags(Set<WC_F> flags) {
    	if (flags == null)
    		throw new IllegalArgumentException("flags == null");
    	if (flags.isEmpty())
    		throw new IllegalArgumentException("flags is empty");
		this.flags = flags;
	}

	/**
	 * Sets the input port.
	 * 
	 * @param input The char input port.
	 */
    public void setInput(final CharInputPort input) {
    	if (input == null)
    		throw new IllegalArgumentException("input == null");
 		this.input = input;
	}

	/**
	 * Sets the output port.
	 * 
	 * @param output The char output port.
	 */
    public void setOutput(final CharOutputPort output) {
    	if (output == null)
    		throw new IllegalArgumentException("output == null");
 		this.output = output;
	}

}
